package nth.bananas.solving;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

public class DefaultBoard implements Puzzle {

	private Set<GridSpot<Letter>> board = new HashSet<GridSpot<Letter>>();;
	private List<Letter> freePieces = new ArrayList<Letter>();

	public DefaultBoard(List<Letter> freePieces) {
		this.freePieces = new ArrayList<Letter>(freePieces);
	}

	@Override
	public void addFreeLetter(Letter tile) {
		freePieces.add(tile);		
	}

	public int totalLetters() {
		return freePieces.size() + board.size() - countOnBoard(null);
	}

	// TODO: only for testing. cocks. And I'm pretty sure this is the only reason freePieces is a list.
	public Letter getRandomTile() {
		int pieceCount = freePieces.size();
		if (pieceCount == 0) {
			return null;
		}

		return freePieces.get(new Random().nextInt(pieceCount));
	}

	@Override
	public Collection<? extends GridSpot<Letter>> adjacentTo(GridSpot<Letter> spot) {
		Set<GridSpot<Letter>> result = new HashSet<GridSpot<Letter>>();

		int[][] coords = new int[4][2];
		coords[0] = new int[] {spot.getX(), spot.getY() + 1};
		coords[1] = new int[] {spot.getX(), spot.getY() - 1};
		coords[2] = new int[] {spot.getX() + 1, spot.getY()};
		coords[3] = new int[] {spot.getX() - 1, spot.getY()};


		for (int[] coord : coords) {
			if (spotExists(coord[0], coord[1])) {
				result.add(getSpot(coord[0], coord[1]));
			}
		}

		return Collections.unmodifiableSet(result);
	}

	@Override
	public Puzzle copyWithoutSpot(GridSpot<Letter> spot) {
		Puzzle result = new DefaultBoard(freePieces);

		for (GridSpot<Letter> ps : board) {
			if (! ps.equals(spot)) { 
				result.addSpot(ps);
			}
		}

		return result;
	}

	@Override
	public Set<GridSpot<Letter>> getSpots() {
		return Collections.unmodifiableSet(board);
	}

	@Override
	public int totalFreePieces() {
		return freePieces.size();
	}

	@Override
	public Puzzle withFilled(GridSpot<Letter> ps) {
		Puzzle result = new DefaultBoard(freePieces);
		result.addSpot(ps);

		for (GridSpot<Letter> gs : board) {
			result.addSpot(gs);
		}

		return result;
	}

	@Override
	public boolean addSpot(GridSpot<Letter> ps) {
		if (ps.getX() < 0 || ps.getY() < 0) {
			throw new IllegalArgumentException(String.format("Coordinates cannot be negative, but (%d, %d) was given.", ps.getX(), ps.getY()));
		}
		if (spotExists(ps.getX(), ps.getY())) {
			throw new IllegalArgumentException(String.format("A spot already exists at (%d, %d): %s", ps.getX(), ps.getY(), getSpot(ps.getX(), ps.getY())));
		}

		return board.add(ps);		
	}

	@Override
	public GridSpot<Letter> getSpot(int x, int y) {
		GridSpot<Letter> result = null;
		for (GridSpot<Letter> gs : board) {
			if (gs.getX() == x && gs.getY() == y) {
				result = gs;
			}
		}

		if (result != null) {
			assert result.getX() == x && result.getY() == y;
		}

		return result;
	}

	@Override
	public boolean spotExists(int x, int y) {
		return getSpot(x, y) != null;
	}

	@Override
	public Puzzle gridUnion(Puzzle other) throws IllegalArgumentException {
		Puzzle result = new DefaultBoard(freePieces); // which set of free pieces do we want to add?

		GridSpot<Letter> otherSpot;
		for (GridSpot<Letter> gs : board) {
			result.addSpot(gs);
			String f = result.toString();
			otherSpot = other.getSpot(gs.getX(), gs.getY()); 
			if (otherSpot != null) {
				if (gs.content() != null && ! gs.content().equals(otherSpot.content())
						|| gs.content() == null && otherSpot.content() != null) {
					throw new IllegalArgumentException(String.format("The two puzzles are contradictory: %s conflicts with %s", otherSpot, gs));
				}
			}
		}

		for (GridSpot<Letter> gs : other.getSpots()) {
			if (! result.spotExists(gs.getX(), gs.getY())) {
				result.addSpot(gs);
			}
		}

		return result;
	}

	@Override
	public boolean removeFreeLetter(Letter r) {
		assert r != null : "Null cannot be removed.";
		if (isOnBoard(r)) {
			return false;
		}

		return freePieces.remove(r);
	}

	private boolean isOnBoard(Letter r) {
		return countOnBoard(r) > 0;
	}

	@Override
	public int countOnBoard(Letter r) {
		int result = 0;
		for (GridSpot<Letter> gs : board) {
			if (r != null && r.equals(gs.content())) {
				result++;
			}
			else if (r == gs.content()) {
				result++;
			}
		}
		return result;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((board == null) ? 0 : board.hashCode());
		result = prime * result + ((freePieces == null) ? 0 : freePieces.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		DefaultBoard other = (DefaultBoard) obj;
		if (board == null) {
			if (other.board != null)
				return false;
		} else if (!board.equals(other.board))
			return false;
		if (freePieces == null) {
			if (other.freePieces != null)
				return false;
		} else if (!freePieces.equals(other.freePieces))
			return false;
		return true;
	}

	@Override
	public void fillOutEmptyBoard() {
		for (int i = 0; i < totalLetters(); i++) {
			for (int j = 0; j < totalLetters(); j++) {
				if (! spotExists(i, j)) {
					addSpot(new GridSpot<Letter>(i, j, null));
				}
			}
		}
	}

	private int largestDimension() {
		int max = 0;
		for (GridSpot<Letter> gs : board) {
			max = Math.max(Math.max(gs.getX(), max), gs.getY());
		}
		return max;
	}

	@Override
	public String toString() {
		String result = "";
		int biggestPossibleDimension = largestDimension();
		String spot = "";

		if (board.isEmpty()) {
			return "empty board";
		}

		for (int y = biggestPossibleDimension; y >= 0; y--) {
			for (int x = 0; x <= biggestPossibleDimension; x++) {
				if (spotExists(x, y)) {
					spot = getSpot(x, y).toString();
				}
				else {
					spot = "";
				}
				result += String.format("%-20s", spot);
			}
			result += String.format("%n");
		}
		return result;
	}

	@Override
	public Puzzle inverse() {
		Puzzle p = new DefaultBoard(freePieces);
		int biggestPossibleDimension = largestDimension();

		for (int x = 0; x <= biggestPossibleDimension; x++) {
			for (int y = 0; y <= biggestPossibleDimension; y++) {
				if (! spotExists(x, y)) {
					p.addSpot(new GridSpot<Letter>(x, y, null));
				}
			}
		}

		return p;
	}

	@Override
	public GridSpot<Letter> getOneSpot() {
		for (GridSpot<Letter> gs : board) {
			return gs;
		}
		return null;
	}

	@Override
	public Collection<Letter> freeLetters() {
		return freePieces;
	}

	@Override
	public Puzzle fromFreeToTiles(GridSpot<Letter> gs, Letter t) {
		assert spotExists(gs.getX(), gs.getY()) : String.format("GridSpot to update [%s] is not in the Puzzle.", gs);
		assert freePieces.contains(t) : String.format("Tried to move a letter from free to tiles that didn't exist: %s", t);
		Puzzle result = new DefaultBoard(freePieces);
		result.removeFreeLetter(t);
		
		for (GridSpot<Letter> spot : board) {
			if (spot.getX() == gs.getX() && spot.getY() == gs.getY()) {
				result.addSpot(new GridSpot<Letter>(gs.getX(), gs.getY(), t));
			}
			else {
				result.addSpot(spot);
			}
		}
		
		return result;
	}

}
